/*
 * Copyright 2012 Claude Houle claude.houle@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.mochaccino.framework.smtp.client;

import com.google.common.base.Preconditions;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Field;
import java.net.SocketException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.net.ssl.SSLContext;
import org.apache.commons.net.smtp.AuthenticatingSMTPClient;
import org.apache.commons.net.smtp.AuthenticatingSMTPClient.AUTH_METHOD;
import org.apache.commons.net.smtp.SMTPClient;
import org.apache.commons.net.smtp.SMTPConnectionClosedException;
import org.apache.commons.net.smtp.SMTPSClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** SMTP Client based on commons-net */
public final class SmtpClient {

	/** Authentication */
	private AUTH_METHOD auth = null;

	/** Slf4j */
	private Logger logger = LoggerFactory.getLogger( getClass() );

	/** SMTP Client */
	private SMTPClient client = null;

	/** Host */
	private String host = null;

	/** Username */
	private String password = null;

	/** Password */
	private String username = null;

	/** Port */
	private int port = 25;

	/** Client */
	public SmtpClient( String host, int port ) {
		this.client = new SMTPClient();
		this.host = Preconditions.checkNotNull( host, "Host is null!" );
		this.port = port;
	}

	/** Constructor */
	public SmtpClient( SSLContext ctx, String host, int port ) {
		this.client = new SMTPSClient( true, ctx );
		this.host = Preconditions.checkNotNull( host, "Host is null!" );
		this.port = port;
	}

	/** Constructor */
	public SmtpClient( String host, int port, String username, String password, AUTH_METHOD auth ) {
		this( null, host, port, username, password, auth );
	}

	/** Constructor */
	public SmtpClient( SSLContext ctx, String host, int port, String username, String password, AUTH_METHOD auth ) {
		this.host = Preconditions.checkNotNull( host, "Host is null!" );
		this.port = port;
		this.auth = Preconditions.checkNotNull( auth, "Authentication method cannot be null..." );
		this.username = Preconditions.checkNotNull( username, "username cannot be null..." );
		this.password = Preconditions.checkNotNull( password, "password cannot be null..." );
		try {
			this.client = new AuthenticatingSMTPClient();

			/**
			 * XXX: This is a reflective hack because the commons-net API do not allow
			 * the authenticating smtp client to supply the SSLContext from the constructor
			 * as it is usually possible from the normla SMTPSClient.
			 */
			Field field = SMTPSClient.class.getDeclaredField( "context" );
			field.setAccessible( true );
			field.set( this.client, ctx );

			field = SMTPSClient.class.getDeclaredField( "isImplicit" );
			field.setAccessible( true );
			field.set( this.client, ctx != null );
		} catch ( NoSuchAlgorithmException e ) {
			throw new RuntimeException( e );
		} catch ( NoSuchFieldException e ) {
			throw new RuntimeException( e );
		} catch ( IllegalAccessException e ) {
			throw new RuntimeException( e );
		}
	}

	/** Connect */
	public void connect() throws IOException {
		Preconditions.checkState( !client.isConnected(), "Client is connected. Therefore could not connect()" );
		client.connect( host, port );
		if ( client instanceof AuthenticatingSMTPClient ) {
			doAuthentication();
		} else if ( !client.login( host ) ) {
			client.disconnect();
			throw new IOException( "Client could not successfully connect to remote server" );
		}
		client.noop();
	}

	private void doAuthentication() throws IOException {
		AuthenticatingSMTPClient c = (AuthenticatingSMTPClient) client;
		if ( !c.elogin( host ) ) {
			client.disconnect();
			throw new IOException( "Client could not successfully connect to remote server" );
		}

		try {
			c.auth( auth, username, password );
		} catch ( NoSuchAlgorithmException e ) {
			client.disconnect();
			throw new IOException( "Client could not successfully authenticate to remote server" );
		} catch ( InvalidKeyException e ) {
			client.disconnect();
			throw new IOException( "Client could not successfully authenticate to remote server" );
		} catch ( InvalidKeySpecException e ) {
			client.disconnect();
			throw new IOException( "Client could not successfully authenticate to remote server" );
		}
	}

	/** Disconnect from the remote STMP server */
	public void disconnect() throws IOException {
		Preconditions.checkState( client.isConnected(), "Client is not connected. Therefore could not disconnect()" );
		client.logout();
		client.disconnect();
	}

	/**
	 * A keep-alive method that allows the socket with remote smtp server to kept-alive. However,
	 * depending on the remote server, it may still kill it because of some security rules.
	 */
	public boolean keepAlive() {
		if ( !isConnected() ) {
			return false;
		}

		try {
			return client.sendNoOp();
		} catch ( SMTPConnectionClosedException e ) {
			logger.error( "Remote server closed prematurely the socket connection..." + e.getMessage(), e );
			return false;
		} catch ( IOException e ) {
			logger.error( "Remote server abruptedly closed the socket connection..." + e.getMessage(), e );
			return false;
		}
	}

	/** Is Connected? */
	public boolean isConnected() {
		return client.isConnected();
	}

	/** Cancel impending commands */
	public boolean reset() {
		Preconditions.checkState( client.isConnected(), "Client is not connected!" );
		try {
			return client.reset();
		} catch ( SMTPConnectionClosedException e ) {
			logger.error( "Remote server closed prematurely the socket connection..." + e.getMessage(), e );
			return false;
		} catch ( IOException e ) {
			logger.error( "Remote server abruptedly closed the socket connection..." + e.getMessage(), e );
			return false;
		}
	}

	/** Send a Message */
	public boolean send( Message message ) {
		try {
			Preconditions.checkState( client.isConnected(), "Client is not connected!" );
			if ( !client.setSender( message.getFrom() ) ) {
				return false;
			}

			String[] recipients = message.getRecipients();
			Preconditions.checkNotNull( recipients, "No recipients!" );
			Preconditions.checkState( recipients.length > 0, "No recipients!" );
			for ( String address : recipients ) {
				if ( !client.addRecipient( address ) ) {
					return false;
				}
			}

			Writer out = client.sendMessageData();
			if ( out == null ) {
				logger.error( "Writer is null... Possibly disconnected" );
				return false;
			}

			message.toWriter( out );
			return client.completePendingCommand();
		} catch ( SMTPConnectionClosedException e ) {
			logger.error( "Remote server closed prematurely the socket connection..." + e.getMessage(), e );
			return false;
		} catch ( IOException e ) {
			logger.error( "Remote server abruptedly closed the socket connection..." + e.getMessage(), e );
			return false;
		}
	}

	public void setConnectTimeout( int connectTimeout ) {
		Preconditions.checkState( !client.isConnected() );
		client.setConnectTimeout( connectTimeout );
	}

	public void setDefaultTimeout( int timeout ) {
		Preconditions.checkState( !client.isConnected() );
		client.setDefaultTimeout( timeout );
	}

	public void setKeepAlive( boolean keepAlive ) throws SocketException {
		Preconditions.checkState( !client.isConnected() );
		client.setKeepAlive( keepAlive );
	}

	public void setReceiveBufferSize( int size ) throws SocketException {
		Preconditions.checkState( !client.isConnected() );
		client.setReceiveBufferSize( size );
	}

	public void setSendBufferSize( int size ) throws SocketException {
		Preconditions.checkState( !client.isConnected() );
		client.setSendBufferSize( size );
	}

	public void setSoLinger( boolean on, int val ) throws SocketException {
		Preconditions.checkState( !client.isConnected() );
		client.setSoLinger( on, val );
	}

	public void setSoTimeout( int timeout ) throws SocketException {
		Preconditions.checkState( !client.isConnected() );
		client.setSoTimeout( timeout );
	}

	public void setTcpNoDelay( boolean on ) throws SocketException {
		Preconditions.checkState( !client.isConnected() );
		client.setTcpNoDelay( on );
	}
}
